﻿#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

// 自定义类型：结构体

// 1.结构体的声明

#if 0
struct tag
{
	member - list;
}variable-list;
#endif

// 我们描述一本书或者一个学生的时候可以这样写

// 书 - 书名，作者，价格，序号
#if 0
struct book
{
	char _book_name[20];
	char _author[20];
	int price;
	char id[20];
};
#endif

//学生 - 姓名，性别，年龄，学号
#if 0
struct Stu
{
	char _name[20];
	char sex[10];
	int age;
	char id[20];
};
#endif

// 2.结构体变量的创建和初始化
#if 0
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};

int main()
{
	// 第一种初始化方式：需要按结构体成员顺序初始化
	struct Stu s1 = { "欧阳",20,"男","20240319" };

	// 第二种初始化方式：可以自定义顺序
	struct Stu s2 = { .age = 21,.sex = "女",.name = "阳区欠",.id = "20240318" };

	//输出S1
	printf("name: %s\n", s1.name);
	printf("age : %d\n", s1.age);
	printf("sex : %s\n", s1.sex);
	printf("id : %s\n", s1.id);

	printf("\n");
	//输出S2
	printf("name: %s\n", s2.name);
	printf("age : %d\n", s2.age);
	printf("sex : %s\n", s2.sex);
	printf("id : %s\n", s2.id);

	return 0;
}
#endif

// 3.结构体的特殊声明

// 3.1匿名结构体类型
#if 0
struct
{
	int a;
	char b;
	double c;
}x;

struct
{
	int a;
	char b;
	double c;
}a[20], *p;
#endif

// 前面两个结构体都省略了结构体的tag（标签）
//
// p = &x; 这行代码合理吗？
//
///*警告：
//编译器会把上⾯的两个声明当成完全不同的两个类型，所以是⾮法的。
//匿名的结构体类型，如果没有对结构体类型重命名的话，基本上只能使⽤⼀次。*/

// 3.2 结构体的自引用
#if 0
struct Stu
{
	int data;
	struct Stu* next;
};
#endif

// 重命名匿名结构体 - typedef
#if 0
typedef struct
{
	int data;
	Stu* nxet;
}Stu;
#endif
 //这样的重命名也会带来问题，就如上面的代码一样是不合法的，因为程序是由上向下运行的，所以在创建Stu* next 时Stu还未创建

 //解决方案：定义的结构体不使用匿名结构体
typedef struct Stu
{
	int data;
	struct Stu* next;
}Stu;


 //4.结构体对齐 - 计算结构体大小

 //4.1对齐规则
 // /*1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
 // 2.其他成员变量要对⻬到某个数字（对⻬数）的整数倍的地址处。
	//对⻬数 = 编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。
	//-VS 中默认的值为 8 
	//-Linux中gcc没有默认对⻬数，对⻬数就是成员⾃⾝的⼤⼩
 // 3.结构体总⼤⼩为最⼤对⻬数（结构体中每个成员变量都有⼀个对⻬数，所有对⻬数中最⼤的）的
	//整数倍。
 // 4.如果嵌套了结构体的情况，嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处，结构
	//体的整体⼤⼩就是所有最⼤对⻬数（含嵌套结构体中成员的对⻬数）的整数倍。*/

 //下面以练习题来了解这四条规则吧

//练习1
#if 0
int main()
{
	struct S1
	{			// 变量本身   vs   对齐数
		char c1;//    1       8     1
		int i;  //    4       8     4
		char c2;//    1       8     1
	};
	printf("%zd\n", sizeof(struct S1));
}
#endif
// 根据规则二我们可以算出每个类型的对齐数

// 根据规则一我们开始画图，从0开始，c1为char类型对齐数为1，所以第一格就可以给他，
// 第二个成员类型为int，对齐数为4，根据规则二前面也需要保障为4的整数倍处，所以在4填涂，然后留出4个格子
// 第三个成员类型为char，对齐数为1，在后面一格填图即可，最后整个结构体的大小要为最多对齐数的整数倍，所以填补到12格

// 以下练习大家可以自己画图计算试试
#if 0
//练习2
int main()
{
	struct S2
	{
		char c1;  // 1 8 1
		char c2;  // 1 8 1
		int i;    // 4 8 4
	};
	printf("%zd\n", sizeof(struct S2));//8

	//练习3
	struct S3
	{
		double d; //8 8 8
		char c;   //1 8 1
		int i;    //4 8 4
	};
	printf("%zd\n", sizeof(struct S3));//16

	//练习4-结构体嵌套问题
	struct S4
	{
		char c1;  // 1 8 1
		struct S3 s3;// 8 8 8
		double d; // 8 8 8
	};
	printf("%zd\n", sizeof(struct S4)); //32

	return 0;
}
#endif

// 4.2为什么存在内存对齐
//
// 1.平台移植原因 ：不是所有的硬件平台都能访问任意地址上的任意数据的；某些硬件平台只能在某些地址处取某些特定
//				   类型的数据，否则抛出硬件异常。
//
// 2.性能原因：数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于，为了访问未对⻬的内存，处理器需要
//作两次内存访问；⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节，则地
//址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数，那么就可以
//⽤⼀个内存操作来读或者写值了。否则，我们可能需要执⾏两次内存访问，因为对象可能被分放在两
//个8字节内存块中。
//
//总结：用空间换时间
//
// 设计结构体时，又想满足对齐规则，又想节省空间，需要尽量将占用空间小的成员集中到一起
//
//如：
#if 0
int main()
{
	struct S1
	{
		char c1;
		int i;
		char c2;
	};

	struct S2
	{
		char c1;
		char c2;
		int i;
	};

	printf("%zd\n", sizeof(struct S1));
	printf("%zd\n", sizeof(struct S2));
	return 0;
}
#endif
// S1的大小占比为12,而S2的大小占比为8。


// 4.3 修改默认对齐

// 使用#pragma 预处理命令，可以改变编译的默认对齐数
#if 0
#pragma pack(1)
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()

int main()
{
	printf("%zd\n", sizeof(struct S)); // 6
	return 0;
}
#endif


// 5.结构体传参
#if 0
struct book
{
	char _book_name[20];
	int price;
};
struct book s1 = { "狂人日记",18 };
//结构体传参
void print1(struct book s1)
{
	printf("%s\n", s1._book_name);
}

//结构体地址传参
void print2(const struct book* ps)
{
	printf("%s\n", ps->_book_name);
}
int main()
{
	print1(s1);
	print2(&s1);
	return 0;
}
#endif

///*上⾯的 print1 和 print2 函数哪个好些？
//答案是：⾸选print2函数。*/
//
// print1 是传值调用，形参，是实参的一份拷贝，这意味着重新开辟一个空间复制一遍实参进行使用，这大大浪费了空间
// print2 是传址调用，使用的数据空间还是原来的，只要根据地址找寻即可，但是为了防止在访问时更改我们需要加上const
//
//
// 6.结构体实现位段

// 6.1什么是位段

 /*位段成员必须是整型，int/ unsigned int/ signed int,在C99中其他类型的位段成员也可以

 位段成员名后面有一个冒号和一个数字*/
#if 0
struct S
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

// S是一个位段类型，那么位段S所占内存大小位多少？
int main()
{
	printf("%zd\n", sizeof(struct S)); // 8
	return 0;
}
#endif

// 6.2 位段的内存分配

//1. 位段的成员可以是 int / unsigned int / signed int 或者是 char 等类型
//2. 位段的空间上是按照需要以4个字节（ int ）或者1个字节（ char ）的⽅式来开辟的。
//3. 位段涉及很多不确定因素，位段是不跨平台的，注重可移植的程序应该避免使⽤位段。

//⼀个例⼦
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
struct S s = { 0 };
int main()
{
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	printf("%zd\n", sizeof(struct S)); //3
	return 0;
}

//空间是如何开辟的？

// a:10 -> 01010   存储： 3 -> 010
// b:12 -> 01100   存储： 4 -> 1100
// c:3  -> 00011   存储： 5 -> 00011
// d:4  -> 00100   存储： 4 -> 0100
// 十进制   二进制

// 图片没有存储的地方补0
//        最后就是 01100010 00000011 00000100
// 转换成16进制即可 0x62      0x03      0x04

// 6.3段位跨平台问题
//1. int位段被当成有符号数还是⽆符号数是不确定的。
//
//2. 位段中最⼤位的数⽬不能确定。（16位机器最⼤16，32位机器最⼤32，写成27，在16位机器会
//出问题。
//
//3. 位段中的成员在内存中从左向右分配，还是从右向左分配标准尚未定义。
//
//4. 当⼀个结构包含两个位段，第⼆个位段成员⽐较⼤，⽆法容纳于第⼀个位段剩余的位时，是舍弃
//剩余的位还是利⽤，这是不确定的。
//
//总结：
//跟结构相⽐，位段可以达到同样的效果，并且可以很好的节省空间，但是有跨平台的问题存在。


// 6.4段位的使用注意事项

//段位是几个成员共用一个字节，所以无法通过scanf取地址输入
#if 0
struct S
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

int main()
{
	struct S s = { 0 };
	//scanf("%d", &s._a); //err

	//正确的方式
	int b = 0;
	scanf("%d", &b);
	s._a = b;

	return 0;
}
#endif











//typedef struct 
//{
//	int a; //  4 8 4
//	char b;//  1 8 1
//	short c;// 2 8 2
//	short d;// 2 8 2
//}AA_t;
//
//int main()
//{
//	printf("%zd\n", sizeof(AA_t));
//	return 0;
//}

//struct A
//{
//	int a; // 4 4 4
//	short b;//2 4 2
//	int c;//  4 4 4
//	char d;// 1 4 1
//};
//
//struct B
//{
//	int a;    //4
//	short b;  //2
//	char c;   //1
//	int d;    //4
//};

